热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

调料|信息源_MVI到底是不是凑数的?通过案例与MVVM进行比较

篇首语:本文由编程笔记#小编为大家整理,主要介绍了MVI到底是不是凑数的?通过案例与MVVM进行比较相关的知识,希望对你有一定的参考价值。 前言 最近看到不少介绍MVI架构,即Model

篇首语:本文由编程笔记#小编为大家整理,主要介绍了MVI到底是不是凑数的?通过案例与MVVM进行比较相关的知识,希望对你有一定的参考价值。



前言

最近看到不少介绍MVI架构,即Model-View-Intent的文章,有人留言说Google炒冷饭或者为了凑KPI“发明”了MVI这么一个词。和后端的朋友描述了一下,他们听了第一印象也是和MVVM好像区别不大。但是凭印象Google应该还没有到需要这样来凑数。

去看了一下官网,发现完全没有提到MVI这个词。。但是推荐的架构图确实是更新了,用来演示MVI也确实很搭。

(官网图)

想了想,决定总结一下自己的发现,和掘友们一起讨论学习。


一丶案例分享

看过一些分析MVI的文章,里面实现的方法各种各样,细节也不尽相同。甚至对于Model边界的划分也会不一样。

下面先分享一下在特定场景下我的MVVMMVI实现(不重要的细节会省略)。


1.1.场景

先预设一个场景,我们的界面(View/Fragment)里有一个锅。主要任务就是完成一道菜的烹饪:

几个需要注意的点:


  • 初始状态:开火
  • 加入材料时:都是异步获取材料,再加入锅中
  • 结束状态:出锅

本文主要是比较MVVMMVI,这里只分享这两种实现。


1.2.经典MVVM

为了加强对比,这里的实现比较接近android Architecture Components刚发布时官网的的代码架构和片段:

(当时的官网图)

// PotFragment.kt
class PotFragment
...
// 观察是否点火
viewModel.fireStatus.observe(
viewLifecycleOwner,
Observer
updateUi()
if (fireOn) addOil()

)
// 观察油温
viewModel.oilTemp.observe(
viewLifecycleOwner,
Observer
updateUi()
if (oilHot) addIngredients()

)
// 观察菜熟没熟
viewModel.ingredientsStatus.observe(
viewLifecycleOwner,
Observer
updateUi()
if (ingredientsCooked)
// 加调料
addPowder(SALT)
addPowder(SOY_SAUCE)


)
// 观察油盐是否加完
viewModel.allPowderAdded.observe(
viewLifecycleOwner,
Observer
// 出锅!

)
viewModel.loading.observe(
viewLifecycleOwner,
Observer
if (loading)
// 颠勺
else
// 放下锅


)
// 一切准备就绪,点火
turnOnFire()
...
// PotViewModel.kt
class PotViewModel(val repo: CookingRepository)
private val _fireStatus = MutableLiveData()
val fireStatus: LiveData = _fireStatus
private val _oilTemp = MutableLiveData()
val oilTemp: LiveData = _oilTemp
private val _ingredientsStatus = MutableLiveData()
val ingredientsStatus: LiveData = _ingredientsStatus
// 所有调料加好了才更新。这里Event内部会有flag提示这个LiveData的更新是否被使用过
//(当年我们还真用这种方式实现过单次消费的LiveData)。
private val _allPowderAdded = MutableLiveData>()
val allPowderAdded: LiveData> = _allPowderAdded
// 假设已经实现逻辑从repo获取是否有还在进行的数据获取
private val _loading = MutableLiveData()
val loading: LiveData = _loading
fun turnOfFire()
// 假设下面都是异步获取材料,这里简化一下代码
fun addOil()
repo.fetchOil()

fun addIngredients()
repo.fetchIngredients()

fun addPowder(val powderType: PowderType)
repo.fetchPowder(powderType)
// 更新_allPowderAdded的逻辑会在这里

...

特点:


  • 使用多个LiveData观察不同的数据,并以此来更新UI。每个LiveData都是一个State,每个View有自己的State
  • UI是否显示loadingRepository决定(是否有正在进行的数据读取)。
  • 对于观察的LiveData要做出何种操作,UI层的逻辑代码往往无法避免。

很久以前也听说过用状态机(state machine)管理UI界面,但是思路还是限制在使用多个LiveData,使用时进行合并。虽然状态更清晰了,但是对于代码的可维护性并没有明显的帮助,甚至ViewModel里还多了些合并LiveData以及状态管理的代码。代码貌似还更复杂了。后来发现了Redux式的思路,才有了下面这个版本的MVI实现。


1.3.MVI

下图是我对这个思路的理解:


  • 单一信息源
  • 单向/环形数据流

定义几个下面代码会用到的名称(不用细究命名,只要自己和团队觉得有意义叫什么都行):


  • State:不管数据从哪里来,经过什么处理,都会归于现在的状态
  • Event:上图中的意图产生或代表的事件,也可以理解为Intent或者Action,最终产生Event让我们更新State
  • Reducer:驱动状态变化的核心。这个例子里可以想象成厨师的手,用来改变锅的状态。
  • Side effects:用户无感知,就当它是“额外效果”(或者“副作用”)。对于数据的请求或者记录上传用户操作的代码都归于此类。

下面开始展示代码:

// PotState.kt
sealed class PotState
object Initial: CookingStatus()
object FireOn: CookingStatus()
class Cooking(val data: List): CookingStatus()
object Finished: CookingStatus()
// CookEvent.kt
sealed class CookEvent
object TurnOnFire(): CookEvent()
object RequestOil(): CookEvent()
object AddOil(): CookEvent()
class RequestIngredient(val ingredientType: IngredientType): CookEvent()
class AddIngredient(val ingredient: Ingredient): CookEvent()
class RequestPowder(val powderType: PowderType): CookEvent()
class AddPowder(val powder: Powder): CookEvent()
object ServeFood()
// models.kt
interface EdibleStuff
data class Oil(...) implements EdibleStuff
data class Ingredient(...) implements EdibleStuff
data class Powder(...) implements EdibleStuff
// PotReducer.kt
class PotReducer
fun reduce(state: PotState, event: CookEvent) =
when (state)
Initial -> reduceInitial(event)
FireOn -> reduceFireOn(event)
is Cooking -> reduceCooking(event)
Finished -> reduceFinished(state, event)

// 每个状态只接受某些特定的Event,其它的会忽略(无法影响当前状态)
private fun reduceInitial(state: PotState, event: CookEvent) =
when (event)
TurnOnFire -> flowOf(FireOn) // 生成一个Cooking状态并加好油
else -> // handle exception

private fun reduceFireOn(state: PotState, event: CookEvent) =
when (event)
AddOil -> flowOf(Cooking(mutableListOf(Oil)) // 生成一个Cooking状态并加好油
else -> // handle exception

private fun reduceCooking(state: PotState, event: CookEvent) =
when (event)
AddIngredient -> flowOf(state.apply data.add(event.ingredient) ) // 加菜
AddPowder -> flowOf(state.apply data.add(event.powder) ) // 加调料
else -> // handle exception

private fun reduceFinished(state: PotState, event: CookEvent) =
when (event)
ServeFood -> flowOf(Finished) // 出锅
else -> // handle exception

// PotViewModel.kt
class PotViewModel(val potReducer: PotReducer, val repo: CookingRepository)
...
var potState: PotState = Initial
// 生成下一状态,更新Flow
fun processEvent(event: CookEvent) =
potReducer.reduce(potState, event)
.updateState()
.handleSideEffects(event)
.launchIn(viewModelScope)
// 对于不直接影响UI的事件,当做side effects处理
private fun handleSideEffects(event: CookEvent) =
onEach event ->
when (event)
is RequestOil -> fetchOil()
is RequestIngredient -> fetchIngredient(...)
is RequestPowder -> fetchPowder(...)


// 收到Repository传来的食料,启动新Event:添加入锅
private fun fetchOil() = repo.fetchOil().onEach processEvent(AddOil) .collect()
// fetchIngredient(...) 与 fetchPowder(...) 也类似
...
// PotFragment.kt
class PotFragment
...
@Composable
fun Pot(viewModel: PotViewModel)
val state by viewModel.potState.collectAsState()
Column
//Render toolbar
Toolbar(...)
//Render screen content
when (state)
FireOn -> // render UI
is Cooking -> // render UI
Finished -> // render UI:出锅!



// 准备就绪,挑个合适的时机开火
viewModel.processEvent(TurnOnFire)
...

特点:


  • Fragment/Activity只负责渲染
  • 用户意图会产生Event,并被ViewModel中的Reducer处理
  • 特定的状态下,只会接收能被处理的Event

二丶分析


2.1.经典MVVM

优点:


  • 相比MVC或者MVP,相信大家都熟悉。

缺点:


  • 每个View有自己的State。很多View混合在一起时,代码和我们的思路都容易变混乱。审核代码也需要对全局有很好的理解。
  • 需要观察的数据多了之后,LiveData管理可以变得很复杂。
  • 可以看到,Fragment中无论何时都在观察并接收所有LiveData的更新。仔细想想,其实这当中是包含了一些逻辑的。比如说,开火之后我们不希望接收加调料的操作。这些逻辑不容易单独拿出来写测试,通常要被包含在Fragment的测试离。

2.2.MVI

优点:


  • Statesingle source of truth,单一信息源,不用担心各个View的状态到处都是,甚至相互冲突。
  • 伴随着预设的状态值,可以接受的意图Intent或者操作Action也可以预设。不在计划里的意图/操作不会对UI界面产生影响,也不会有额外效果。审核代码只需要了解新增的意图对某一两个受影响的状态就足够,不用把整个界面的内容都复盘一遍。单元测试也是类似。也算是符合关注点分离(Separation of Concerns)。

缺点:


  • 随着View变得复杂,可以有的状态以及能接受的意图也会迅速膨胀。
  • 文件数量变多(这个和从MVC到MVP的感觉有点像)。
  • 新手学习、理解起来不容易。

2.3.比较

两种架构都有优缺点。

因为大家都熟悉MVVM,新团队的接受度肯定会好。

有些缺点也可以想办法改进。例如MVI的状态膨胀可以通过划分为几个小的分状态来缓解。

对于复杂的场景,我个人更倾向于采用MVI全局状态管理的思路。主要还是觉得传统MVVM每次添加新的LiveData时(当然现在常常用Flow),需要仔细检查其它所有的View或者LiveData,生怕漏掉什么改动,不利于高效开发和维护。


三丶总结

我认为传统的MVVMMVI主要的区别还是在于全局状态管理。而且这个全局状态管理的思路用传统MVVM架构也能实现,很多人觉得MVIMVVM差不多的原因可能正是如此。 其实也不足为奇,不少设计模式两两之间也很相似,但并不妨碍大家给他们安上不同的名字。只要我们把握住核心概念,合理运用,叫什么名字也不重要。正如官方的建议:

就算叫MVI只是为了唬人,让人一听到就知道你运用了Redux/State machine的思路,而不是“经典”的安卓版MVVM,好像也是个不错的理由。


题外话

从官网架构图的变化产生的联想:


ViewModel 化身 LifecycleObserver

最近看到不少文章分享他们对于让ViewModellifecycle-aware的实验。从官方文档看,UI elementsState holders(在我看来就是Fragment/ActivityViewModel)也在被视作一个整体的UI Layer。不知道以后是不是会有这么一个趋势。

有时候,不经意间就会错过一些有趣实用的想法。回想2017年的时候,听到WeWork的员工分享他们自制的Declarative UI库。当时觉得都不能预览,应该不会好用到哪去吧。没想到后来官方发布了Compose,预览功能都加入了Android Studio


选择性使用的 Domain Layer

也许是随着这几年Clean Architecture的热度上升,看到不少团队开始加入领域层。官方推荐的架构图(开头提到)中也加入了Domain Layer (optional)。添加这么一层的确可以帮助我们解耦部分逻辑。



作者:厨师小p
链接:https://juejin.cn/post/7090200118431121416
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



推荐阅读
  • 本文介绍了如何使用JavaScript的Fetch API与Express服务器进行交互,涵盖了GET、POST、PUT和DELETE请求的实现,并展示了如何处理JSON响应。 ... [详细]
  • Explore a common issue encountered when implementing an OAuth 1.0a API, specifically the inability to encode null objects and how to resolve it. ... [详细]
  • [论文笔记] Crowdsourcing Translation: Professional Quality from Non-Professionals (ACL, 2011)
    Time:4hoursTimespan:Apr15–May3,2012OmarZaidan,ChrisCallison-Burch:CrowdsourcingTra ... [详细]
  • 本文详细介绍 Go+ 编程语言中的上下文处理机制,涵盖其基本概念、关键方法及应用场景。Go+ 是一门结合了 Go 的高效工程开发特性和 Python 数据科学功能的编程语言。 ... [详细]
  • 本文基于刘洪波老师的《英文词根词缀精讲》,深入探讨了多个重要词根词缀的起源及其相关词汇,帮助读者更好地理解和记忆英语单词。 ... [详细]
  • 360SRC安全应急响应:从漏洞提交到修复的全过程
    本文详细介绍了360SRC平台处理一起关键安全事件的过程,涵盖从漏洞提交、验证、排查到最终修复的各个环节。通过这一案例,展示了360在安全应急响应方面的专业能力和严谨态度。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 5G至4G空闲态移动TAU流程解析
    本文详细解析了用户从5G网络移动到4G网络时,在空闲态下触发的跟踪区更新(TAU)流程。通过N26接口实现无缝迁移,确保用户体验不受影响。 ... [详细]
  • 版本控制工具——Git常用操作(下)
    本文由云+社区发表作者:工程师小熊摘要:上一集我们一起入门学习了git的基本概念和git常用的操作,包括提交和同步代码、使用分支、出现代码冲突的解决办法、紧急保存现场和恢复 ... [详细]
  • 解决vCenter vSphere HA初始化失败的问题
    本文探讨了在集群中遇到的所有vSphere HA主机状态显示‘无法正确安装或配置vSphere HA代理’错误的情况,并详细介绍了排查与解决步骤,包括检查HA初始化错误及安装HA代理的常见故障排除方法。 ... [详细]
  • 探讨如何高效使用FastJSON进行JSON数据解析,特别是从复杂嵌套结构中提取特定字段值的方法。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • 1.如何在运行状态查看源代码?查看函数的源代码,我们通常会使用IDE来完成。比如在PyCharm中,你可以Ctrl+鼠标点击进入函数的源代码。那如果没有IDE呢?当我们想使用一个函 ... [详细]
  • 本文详细介绍了如何通过RPM包在Linux系统(如CentOS)上安装MySQL 5.6。涵盖了检查现有安装、下载和安装RPM包、配置MySQL以及设置远程访问和开机自启动等步骤。 ... [详细]
  • 本文详细介绍了优化DB2数据库性能的多种方法,涵盖统计信息更新、缓冲池调整、日志缓冲区配置、应用程序堆大小设置、排序堆参数调整、代理程序管理、锁机制优化、活动应用程序限制、页清除程序配置、I/O服务器数量设定以及编入组提交数调整等方面。通过这些技术手段,可以显著提升数据库的运行效率和响应速度。 ... [详细]
author-avatar
鹏城飞将
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有